Skip to content

feat(cm): mirror the critical-minerals workbook into CM_legacy - #867

Draft
jirhiker wants to merge 5 commits into
stagingfrom
feat/critical-minerals-legacy-mirror
Draft

feat(cm): mirror the critical-minerals workbook into CM_legacy#867
jirhiker wants to merge 5 commits into
stagingfrom
feat/critical-minerals-legacy-mirror

Conversation

@jirhiker

Copy link
Copy Markdown
Member

Phase-1 staging mirror of the Earth MRI critical-minerals chemistry workbook (McLemoreMasterChem, compiled by V.T. McLemore et al., NMBGMR) into CM_* tables, plus a reconciliation report for the source-data owner. No transform into the Ocotillo data model yet.

What changed

Seven mirror tables, following the db/nma_legacy.py / db/nmw_legacy.py convention:

Source sheet Mirror table
ChemicalData / GIS / QAQC CM_ChemicalData (source_sheet discriminator)
DetectionLimits CM_DetectionLimits
References CM_References
MineralSystems CM_MineralSystems
world / world_ref CM_WorldComparisons / CM_WorldReferences
General Information / MetaData / DefinitionOfFields CM_WorkbookMetadata
  • db/cm_legacy.py — models, plus SOURCE_HEADER_BY_COLUMN and ANALYTE_UNITS
  • alembic/versions/d4e5f6a7b8c9_cm_legacy_mirror_tables.py — migration, single head
  • services/cm_legacy_mirror.py — loader, idempotent per sheet
  • scripts/cm_reconciliation_report.py — builds the decision workbook
  • oco load-critical-minerals-workbook --file <xlsx>
  • docs/critical-minerals-legacy-mirror.md, CLAUDE.md pointer
  • 51 tests across tests/test_cm_legacy.py and tests/test_cm_reconciliation_report.py

Why it looks like this

Three sheets, one table. ChemicalData and GIS are byte-identical in header text and order (118 columns each); QAQC is those columns minus MapSymbol/Pd/Pt with latitude/longitude capitalized. Only the layout differs, so one table behind a source_sheet discriminator beats three near-duplicate tables — and it makes reconciliation a self-join.

Rows are keyed on (source_sheet, source_row), not SAMPLE. Sample names repeat across 258 distinct names in the source, so SAMPLE cannot be the key. source_row is the 1-based Excel row number, which makes every mirror row traceable to a cell range in the delivered workbook and a reload idempotent per sheet.

Every column is a String. The workbook stores censored analyte values as text (1154 < values in Au alone), carries #VALUE! errors, and mixes real dates with year-only text. Any typed column drops cells; parsing value-plus-qualifier and casting dates belong to the transform.

Column names are derived, not copied. Sheet headers are spreadsheet labels, not SQL identifiers (Chem Lab File No., Depth/legnth (ft), H2O+), so unlike the NMA/NMW mirrors the source name cannot be reused verbatim. Analytes carry the unit the workbook declares for them (sio2_pct, au_ppb, as_ppm), which also keeps As and In off the Python and SQL keyword lists. SOURCE_HEADER_BY_COLUMN holds the exact header for all 118 columns.

The load asserts its layout. Header rows are declared per sheet (ChemicalData 2, GIS 1, QAQC 2); a moved header row or an unmapped column aborts the load with nothing written, rather than silently dropping cells. A revised workbook that changes shape needs a human and a migration.

Reviewer should know: reconciliation is deferred on purpose

GIS is not ChemicalData plus location data, and not a clean subset. It is a stale hand-maintained fork: same mixed datums, same ~876 rows with no latitude, so it adds no location information. Every GIS sample name exists in ChemicalData, but 1704 of the 4848 shared rows disagree in both directions:

Direction Columns
GIS has values ChemicalData lacks 533 Chem Lab File No., 485 Laboratory, 85 FeO, 85 Fe2O3
ChemicalData has values GIS lacks 633 Total, 184 Area, 11 TREE, 4 Date analyzed
Outright disagreement 16 rows on Area (ZuniMountains vs Zuni)
Broken formulas 12 #VALUE! Totals in GIS, 1 in ChemicalData

Plus 18 sample rows appended to ChemicalData after GIS was last synced — all Pearce (2020), all with coordinates.

Neither sheet is authoritative, so both are mirrored in full and the merge is left to a per-column ruling by V.T. McLemore. A query reading only source_sheet='ChemicalData' silently drops 533 lab file numbers, 485 lab names and 170 FeO/Fe2O3 values. The loader warns whenever the two row counts diverge.

The reconciliation report

scripts/cm_reconciliation_report.py writes an 8-sheet decision workbook: ColumnDecisions (8), CellDifferences (2123), RowsOnlyInOneSheet (19), IntegritySummary (13), IntegrityDetail (4276), DetectionLimitSpread (64), DuplicateSampleNames (258). Every judgement column is blank with a dropdown.

Integrity findings from ChemicalData alone include 724 values impossible for their declared unit (F = 27700 in a % column), 259 Au detection limits that are ppm-scale in a ppb column, 300 non-numeric analyte tokens (bd, nd, tr, nr, ----, >2%), 159 Totals that disagree with the sum of the majors, and 64 of 84 analytes reported against more than three distinct detection limits.

Total is deliberately exempt from the unit check — it is a sum, so exceeding 100 is not itself a unit error; flagging it added 1519 false positives. Only ChemicalData is integrity-checked, since running the same checks over GIS would double every finding without adding information.

Verification

  • Full suite green: 1070 passed, 81 skipped, 6 xpassed
  • Migration DDL executed against the test database in a rolled-back transaction; all 7 tables match the model metadata column-for-column
  • Loaded the delivered workbook end to end: 10,100 rows (ChemicalData 4867, GIS 4848, QAQC 8, DetectionLimits 61, References 81, MineralSystems 169, world 7, world_ref 4, metadata 24/9/22)
  • 708 cells across 6 random rows compared against openpyxl with zero mismatches. Full float precision survives (101.83999999999999, not 101.84), censored values keep their qualifier, #VALUE! passes through, dates become ISO-8601 instead of Excel serials

Notes

  • Nothing here reads or writes any existing table; the mirror is additive
  • services/cm_legacy_mirror.py exposes cell_to_text, normalize_header and column_by_header publicly so the report reuses the mirror's exact cell rendering rather than reimplementing it
  • Loading is manual (oco load-critical-minerals-workbook) — like the other legacy mirrors, there is no CD path for data loads

🤖 Generated with Claude Code

Phase-1 staging mirror of the Earth MRI critical-minerals chemistry workbook
(McLemoreMasterChem, NMBGMR) into CM_* tables, plus a reconciliation report for
the source-data owner. No transform into the Ocotillo data model yet.

Seven mirror tables. The ChemicalData, GIS and QAQC sheets share a column set,
so they land in one CM_ChemicalData behind a source_sheet discriminator: the
first two are byte-identical in header text and order, and QAQC is those columns
minus MapSymbol/Pd/Pt with latitude/longitude capitalized. Rows are keyed on
(source_sheet, source_row) rather than SAMPLE, which repeats across 258 names.

GIS is a stale hand-maintained fork of ChemicalData, not a location-enriched
copy: it carries the same mixed datums and the same ~876 rows with no latitude,
and 1704 of the 4848 shared rows disagree in both directions (GIS holds 533
Chem Lab File No., 485 Laboratory and 170 FeO/Fe2O3 values ChemicalData lacks;
ChemicalData holds 633 Total, 184 Area and 18 appended Pearce (2020) samples GIS
lacks). Neither sheet is authoritative, so both are mirrored in full and
reconciliation is deferred to a per-column ruling by V.T. McLemore. The loader
warns whenever the two row counts diverge.

Every column is a String. The workbook stores censored analyte values as text
(1154 '<' values in Au alone), carries '#VALUE!' errors, and mixes real dates
with year-only text; parsing belongs to the transform. Column names are derived
mechanically because sheet headers are not SQL identifiers, with analytes
carrying the unit the workbook declares for them - which also keeps As and In
off the Python and SQL keyword lists.

Loading is idempotent per sheet and asserts the layout instead of guessing it: a
moved header row or an unmapped column aborts the load rather than silently
dropping cells.

scripts/cm_reconciliation_report.py builds the decision workbook - the
sheet-to-sheet drift plus 4276 integrity findings across 13 issue types, among
them 724 values impossible for their declared unit (F = 27700 in a % column),
259 Au detection limits that are ppm in a ppb column, 300 non-numeric analyte
tokens, and 64 of 84 analytes reported against more than three distinct
detection limits.

Verified by loading the delivered workbook: 10,100 rows, and 708 cells across 6
random rows compared against openpyxl with zero mismatches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Coverage

79.55% total — gate is 75%.

Coverage for the Python files changed in this PR
Name Stmts Miss Cover Missing
cli/cli.py 650 290 55% 59, 63, 67-71, 85, 253-255, 286, 290, 301-304, 307-309, 343-448, 496-552, 669-670, 677, 691, 707, 716, 721, 726-727, 763-769, 774-776, 783-784, 792, 800, 810-817, 822-823, 828, 830-831, 907-911, 921, 923, 933, 946, 951-952, 955, 986-1072, 1097-1163, 1189-1201, 1261-1290, 1381-1393, 1397
db/__init__.py 59 1 98% 84
db/cm_legacy.py 221 0 100%
services/cm_legacy_mirror.py 142 6 96% 154, 167, 185, 199, 228, 276
TOTAL 1072 297 72%

1 similar comment
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Coverage

79.55% total — gate is 75%.

Coverage for the Python files changed in this PR
Name Stmts Miss Cover Missing
cli/cli.py 650 290 55% 59, 63, 67-71, 85, 253-255, 286, 290, 301-304, 307-309, 343-448, 496-552, 669-670, 677, 691, 707, 716, 721, 726-727, 763-769, 774-776, 783-784, 792, 800, 810-817, 822-823, 828, 830-831, 907-911, 921, 923, 933, 946, 951-952, 955, 986-1072, 1097-1163, 1189-1201, 1261-1290, 1381-1393, 1397
db/__init__.py 59 1 98% 84
db/cm_legacy.py 221 0 100%
services/cm_legacy_mirror.py 142 6 96% 154, 167, 185, 199, 228, 276
TOTAL 1072 297 72%

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Phase-1 staging mirror for the Earth MRI critical-minerals chemistry workbook (McLemoreMasterChem) into new CM_* PostgreSQL tables, plus a reconciliation workbook generator to help the source-data owner resolve ChemicalData vs GIS drift before any transform into the Ocotillo domain model.

Changes:

  • Introduces CM_legacy SQLAlchemy models and an Alembic migration creating 7 mirror tables (string-typed, keyed by (source_sheet, source_row) where applicable).
  • Implements an idempotent workbook loader (services/cm_legacy_mirror.py) and exposes it via a new CLI command (oco load-critical-minerals-workbook).
  • Adds a reconciliation report script and comprehensive tests + documentation for the new mirror layer.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
db/cm_legacy.py Defines CM_* mirror models and source-header/unit mappings for faithful workbook mirroring.
alembic/versions/d4e5f6a7b8c9_cm_legacy_mirror_tables.py Creates the CM_* mirror tables and indexes via migration.
services/cm_legacy_mirror.py Loads workbook sheets into mirror tables (string rendering + per-sheet idempotency).
cli/cli.py Adds oco load-critical-minerals-workbook --file <xlsx> to run the mirror load.
scripts/cm_reconciliation_report.py Generates a decision workbook comparing ChemicalData vs GIS and integrity findings.
docs/critical-minerals-legacy-mirror.md Documents the mirror design, drift rationale, and reconciliation workflow.
tests/test_cm_legacy.py Tests loader behavior, schema/header mappings, and idempotent per-sheet reload semantics.
tests/test_cm_reconciliation_report.py Tests reconciliation workbook structure, counts, and integrity classifications.
db/__init__.py Registers the new cm_legacy models in the DB package imports.
CLAUDE.md Adds repository guidance/pointer for the new CM_legacy mirror layer.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/critical-minerals-legacy-mirror.md Outdated
Comment thread scripts/cm_reconciliation_report.py
Comment thread services/cm_legacy_mirror.py
jirhiker and others added 4 commits August 20, 2026 20:59
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Coverage

79.54% total — gate is 75%.

Coverage for the Python files changed in this PR
Name Stmts Miss Cover Missing
cli/cli.py 650 290 55% 59, 63, 67-71, 85, 253-255, 286, 290, 301-304, 307-309, 343-448, 496-552, 669-670, 677, 691, 707, 716, 721, 726-727, 763-769, 774-776, 783-784, 792, 800, 810-817, 822-823, 828, 830-831, 907-911, 921, 923, 933, 946, 951-952, 955, 986-1072, 1097-1163, 1189-1201, 1261-1290, 1381-1393, 1397
db/__init__.py 59 1 98% 84
db/cm_legacy.py 221 0 100%
services/cm_legacy_mirror.py 147 8 95% 154, 167, 185, 199, 228, 276, 341-342
TOTAL 1077 299 72%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants